/* Andreas Baum
 * ServerThread.java
 * Handles the interaction between the server and a client
 * CS 78 - Networks - Programming Assignment 1
 */

package ChatServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class ServerThread extends Thread {
	private Socket socket = null;
	private PrintWriter out = null;
	private BufferedReader in = null;
	private String username;
	private Boolean isSilenced = false;
	ConnectionManager cm;
	private String[] history;
	private int MAX_HISTORY = 30;

	// This list contains all valid keywords and
	// their correpsonding emotes:
	private final String[] emoteList = {
			"LOL", "laughs out loud.",
			"JK", "is just kidding.",
			"FDL", "falls down laughing.",
			"SMILE", "smiles.",
			"GRIN", "grins.",
			"NOD", "nods.",
			"STFU", "asks that you be quiet.",
			"WINK", "winks at you.",
			"YAY", "says \"Yay!\"",
			"WOOT", "goes WOOT WOOT WOOT!",
			"BLAH", "blahs.",
			"WEEP", "weeps.",
			"CRY", "cries.",
			"GIGGLE", "giggles.",
			"GRRR", "grrrs.",
			"SNICKER", "snickers.",
			"CACKLE", "cackles with glee.",
			"DANCE", "dances to a tune nobody else can hear.",
			"JUMP", "jumps up and down.",
			"SIMLE", "tries to smile but forgets how to type.",
			"SMLIE", "tries to smile but forgets how to type.",
			"GRNI", "tries to grin but forgets how to type.",
			"GIRN", "tries to grin but forgets how to type.",
			"WOOHOO", "goes \"Woohoo!\".",
			"GTG", "says \"I gotta go.\"",
			"BRB", "will be right back."
	};

	public ServerThread(Socket socket) {
		super("ServerThread");
		this.socket = socket;

		// get the connection manager, which handles all the 
		// information about current users, and the interaction 
		// between users
		cm = ConnectionManager.getConnectionManager();

		history = new String[MAX_HISTORY];

		try {

			out = new PrintWriter(socket.getOutputStream(), true);
			in = new BufferedReader(
					new InputStreamReader(socket.getInputStream()));
			int userOK = 0;

			while (userOK == 0) {
				// Receive the username from the client connecting
				// only accept the first word and trim white space:
				username = in.readLine().split(" ")[1];

				// Check if username is "appropriate":
				if (username.length() > 10) {
					sendError("Username is too long. Please limit to 10 characters.");
					System.out.println("User rejected: " + username + ". Name too long.");
				}
				// Check if username is already used:
				else if (!cm.doesNameExist(username)) {
					userOK = 1;
					// If username is not used, inform server of login.
					// System.out.println(username + " has joined the server.");
					// Update user list, and inform other users:
					cm.logInUser(username, this);
					// Inform user of successful connection:
					sendInfo("You are now logged in as " + username + ". Type ? for help.");
					// Start thread to listen to the socket:
					this.start();
				}
				else {
					// If username was already taken, inform server and user:
					System.out.println("Warning: A user tried to log in as " + username +", but name is already used.");
					sendError("Username already exists.");
					// Inform server that user was disconnected.
					System.out.println("User posing as " + username + " was removed.");
					// Close streams and socket, as this thread is no longer needed:
					userOK = 0;
				}
			}

		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}



	}

	public void run() {

		String line = null;

		try {				

			// when the socket receives something:
			while ( ( ( line = in.readLine() ) != null ) && !socket.isClosed(  ) ) {

				// Remove white space at beginning and end of user entry:
				line = line.trim();

				// if the user just hit enter, do nothing, 
				// wait for next input
				if (line.equals(""))
					continue;

				// If the user issued a keyword command, it is the first word:
				String command = line.split(" ")[0].toUpperCase();

				// If the user entered only a single-word command, 
				// check for keyword matches:
				if (line.toUpperCase().equals(command)) {

					// If the user requested the help file:
					if ((command.equals("?")) || (command.equals("HELP"))) {
						this.sendHelp();
						continue;
					}

					// If the user has requested the buddy list:
					if ((command.equals("-GET")) || (command.equals("-WHO"))) {
						Object[] buddies = cm.getBuddyList();
						int i = 0;
						this.sendInfo("Users currently connected:");
						while (i < buddies.length) {
							String buddylist = (String) buddies[i] + " ";
							i++;
							// only put four users per line:
							while ((i % 4 != 0) && (i<buddies.length)){
								buddylist = buddylist.concat(((String) buddies[i]) + " ");
								i++;
							}
							// send a line of users:
							this.sendInfo(buddylist);

						}
//						// inform server that user requested the buddy list: 
						System.out.println(username + " requested the buddy list.");
						line = null;
						continue;
					}

					if (command.equals("-HIST")) {
						sendHistory();
						continue;
					}

					// If the user has requested to leave the server:
					if ((command.equals("-EXT") || (command.equals("-QUIT"))) 
							|| (command.equals("-EXIT"))) {
						this.disconnect();
						line = null;
						break;
					}

					// If the user is silenced, ignore his single word:
					if (isSilenced == true) {
						sendInfo("You can't use the main channel when silenced. Use private messages.");
						continue;
					}

					if (username.equals(AdminClient.ADMIN) && (command.equals("SIMPSONS"))) {
						cm.broadcastMessage(username, ".              ,---.    ");
						cm.broadcastMessage(username, ".            ,.'-.   \\   ");
						cm.broadcastMessage(username, ".           ( ( ,'\"\"\"\"\"-.   ");
						cm.broadcastMessage(username, ".           `,X          `.   ");
						cm.broadcastMessage(username, ".           /` `           `._   ");
						cm.broadcastMessage(username, ".          (            ,   ,_\\   ");
						cm.broadcastMessage(username, ".          |          ,---.,'o `.   ");
						cm.broadcastMessage(username, ".          |         / o   \\     )   ");
						cm.broadcastMessage(username, ".           \\ ,.    (      .____,   ");
						cm.broadcastMessage(username, ".            \\| \\    \\____,'     \\   ");
						cm.broadcastMessage(username, ".          '`'\\  \\        _,____,'   ");
						cm.broadcastMessage(username, ".          \\  ,--      ,-'     \\   ");
						cm.broadcastMessage(username, ".            ( C     ,'         \\   ");
						cm.broadcastMessage(username, ".             `--'  .'           |   ");
						cm.broadcastMessage(username, ".               |   |         .O |   ");
						cm.broadcastMessage(username, ".             __|    \\        ,-'_   ");
						cm.broadcastMessage(username, ".            / `L     `._  _,'  ' `.   ");
						cm.broadcastMessage(username, ".           /    `--.._  `',.   _\\  `   ");
						cm.broadcastMessage(username, ".           `-.       /\\  | `. ( ,\\  \\   ");
						cm.broadcastMessage(username, ".          _/  `-._  /  \\ |--'  (     \\   ");
						cm.broadcastMessage(username, ".         '  `-.   `'    \\/\\`.   `.    )   ");
						cm.broadcastMessage(username, ".               \\  -hrr-    \\ `.  |    |  ");
						continue;
					}

					// Check if single-word command is an emote keyword:
					if (checkEmotes(command))
						continue;

					// By default, If single-word command was not a 
					// keyword, broadcast it:
					if (!line.equals("null")) {
						cm.broadcastMessage(username, line);
					}
					continue;
				}

				// If command is INF, send info message to client:
				if (command.equals("INF")) {
					sendInfo(line.substring(3));
					line = null;
					continue;
				}

				// If command is ERR, send error message to client:
				if (command.equals("ERR")) {
					sendError(line.substring(3));
					line = null;
					continue;
				}

				// If the command is MSG, this is a private message to a user:
				if (command.equals("MSG")) {
					String user = line.split(" ")[1].toLowerCase();
					if (cm.doesNameExist(user)) {
						// Inform the server that a message was sent
						// TODO: consider removing for privacy. (Good for testing)
						System.out.println(username + " sent " + user + " a message.");
						String msg = line.substring(5 + user.length());
						sendEcho(user, msg);
						cm.sendMessage(user, username, msg);
					}
					else {
						sendError(user + " is not an existing user.");
					}
					line = null;
					continue;
				}

				// ADMIN commands only
				if (username.equals(AdminClient.ADMIN)){

					if (command.equals("FORCEKICK")) {
						String user = line.split(" ")[1].toLowerCase();
						if (cm.doesNameExist(user)) {
							// Inform the server that a message was sent
							// TODO: consider removing for privacy. (Good for testing)
							cm.bootUser(line.split(" ")[1], username);
						}
						else {
							sendError(user + " is not an existing user.");
						}
						continue;
					}

					if (command.equals("SILENCE")) {
						String user = line.split(" ")[1].toLowerCase();
						if (cm.doesNameExist(user)) {
							// Inform the server that a message was sent
							// TODO: consider removing for privacy. (Good for testing)
							cm.silenceUser(line.split(" ")[1], username);
						}
						else {
							sendError(user + " is not an existing user.");
						}
						continue;
					}

					if (command.equals("UNSILENCE")) {
						String user = line.split(" ")[1].toLowerCase();
						if (cm.doesNameExist(user)) {
							// Inform the server that a message was sent
							// TODO: consider removing for privacy. (Good for testing)
							cm.unsilenceUser(line.split(" ")[1], username);
						}
						else {
							sendError(user + " is not an existing user.");
						}
						continue;
					}

				}

				if (isSilenced == true) {
					sendInfo("You can't use the main channel when silenced. Use private messages.");
					continue;
				}

				// If command is SAY or SHOUT, broadcast global message:
				if ((command.equals("SAY")) || (command.equals("SHOUT"))) {
					cm.broadcastMessage(username, line.substring(command.length()+1));
					continue;
				}

				// If the command is "EMOTE", broadcast globally as an emote:
				if ((command.equals("EMOTE") || (command.equals(":")))) {
					cm.broadcastEmote(username, line.substring(command.length() + 1));
					continue;
				}

				// By default, if the single word does not resolve to a 
				// known command, broadcast it as a simple message
				cm.broadcastMessage(username, line);
			}

			if (socket.isClosed()) {
				//System.out.println(username + " has disconnected.");
				cm.logOutUser(username,false);
				in.close();
				out.close();
				this.socket.close();
			}
		} catch (Exception e) {
			try {
				// if error, log the user out and inform server:
				//System.out.println(username + " has disconnected.");
				cm.logOutUser(username, false);
				in.close();
				out.close();
				this.socket.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		} 
		// If socket is closed, but the user is still in the user list
		// log the user out and inform the server that the user has been 
		// disconnected:
		if (cm.doesNameExist(username)){
			System.out.println(username + " has disconnected.");
			cm.logOutUser(username, false);
		}
	}

	// Sends an error message:
	public void sendError(String string) throws IOException{
		System.out.println("<<ERROR: " + string + ">>");
		out.println("ERR <<ERROR: " + string + ">>");
		out.flush();	
	}

	// Sends an info message:
	public void sendInfo(String string) throws IOException{
		out.println("PRINT :: " + string);
		out.flush();
	}

	// Sends a global message or emote:
	public void sendGlobal(String string) throws IOException {
		out.println("PRINT " + string);
		addToHistory(string);
	}

	// Sends the message sent by another user:
	public void sendMessage(String sender, String string) throws IOException{
		String PrePMSend = "++ PM FROM ";
		int number_of_spaces = 25 - sender.length() - PrePMSend.length();
		String spaces = "";
		int i;
		for (i = 0; i< number_of_spaces; i++) {
			spaces = spaces + " ";
		}
		PrePMSend = PrePMSend + sender + spaces;
		addToHistory(PrePMSend + string);
		String PMSend = "PRINT " + PrePMSend;
		out.println(PMSend + string);
		out.flush();
	}

	// Echos the message sent by this user:
	public void sendEcho(String destination, String string) throws IOException{
		String PrePMSend = "++ PM TO ";
		int number_of_spaces = 25 - destination.length() - PrePMSend.length();
		String spaces = "";
		int i;
		for (i = 0; i< number_of_spaces; i++) {
			spaces = spaces + " ";
		}
		PrePMSend = PrePMSend + destination + spaces;
		addToHistory(PrePMSend + string);
		String PMSend = "PRINT " + PrePMSend;
		out.println(PMSend + string);
		out.flush();
	}

	// This method sends the user the help file, if he requests it:
	public void sendHelp() {
		System.out.println(username + " requested help.");						
		try {
			sendInfo("Valid commands are:");
			sendInfo("\t-ext or -exit or -quit");
			sendInfo("\t\tdisconnect");
			sendInfo("");

			sendInfo("\t-get or -who");
			sendInfo("\t\trequest buddy list");
			sendInfo("");

			sendInfo("\tmsg");
			sendInfo("\t\te.g \"msg andreas hello!\" to send a private message to andreas"); 
			sendInfo("");

			sendInfo("\tsay");
			sendInfo("\t\te.g. \"say hello!\" to broadcast to everyone");
			sendInfo("\t\tThe absence of a known command will default to a global message.");
			sendInfo("");

			sendInfo("\temote or \":\"");
			sendInfo("\t\te.g. \": says hello!\" to emote to everyone");
			sendInfo("");

			sendInfo("\tfdl, lol, stfu, jk, grin, wink, nod, smile, etc.");
			sendInfo("\t\tglobal emotes");
			sendInfo("");

			sendInfo("\thelp or ?");
			sendInfo("\t\tdisplay this message again");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// This method checks if the user entered a known emote as 
	// a single-word keyword. If so, it broadcasts the appropriate emote
	// and returns true. Otherwise, returns false.
	public boolean checkEmotes(String command) {
		int i = 0;
		for (i = 0; i < emoteList.length; i++) {
			// Check every emote:
			if (command.equals(emoteList[i])){
				// Broadcast the appropriate emote:
				cm.broadcastEmote(username, emoteList[i+1]);
				return true;
			}
			i++;
		}
		return false;
	}

	// Disconnects the user after sending feedback to the server.
	public void disconnect() {
		try {
			out.println("EXT");
			// Update the user list:
			cm.logOutUser(username, false);
			out.flush();
			// Send feedback to server:
			System.out.println(username + " just logged out.");
			// Close all streams and socket:
			out.close();
			in.close();
			this.socket.close();
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}

	}

	// This allows an admin to forcefully remove a user.
	// Same as disconnect(), but sends a different message
	// to the server.
	public void boot(String sender) {
		try {
			out.println("EXT");
			// Update the user list:
			cm.logOutUser(username, false);
			out.flush();
			// Send feedback to server:
			System.out.println(username + " was booted by the administrator.");
			// Close all streams and socket:
			out.close();
			in.close();
			this.socket.close();
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
	}

	// This silences a user
	public void silence() {
		isSilenced = true;
		try {
			sendInfo("The administrator got tired of you and tied your tongue in a knot.");
			sendInfo("You've been silenced by the administrator.");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// This unsilences a user
	public void unsilence() {
		isSilenced = false;
		try {
			sendInfo("The administrator has generously granted you the gift of speech.");
			sendInfo("You are no longer silenced.");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// This sends the history to the user
	public void sendHistory() {
		try {
			int i;
			sendInfo("HISTORY:");
			if (history.length < 1) {
				sendInfo("There is no history yet.");
			}
			else { 
				for (i=0;i<history.length;i++) {
					if (history[i] != null) {
						sendInfo("[" + (i+1) + "] " + history[i]);
					}
				}
			System.out.println(username + " requested the history.");
			}
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
	}

	// This adds a message to the history
	public void addToHistory(String line) {
		int i = history.length;
		for (i=0;i<MAX_HISTORY-1;i++) {
			history[i] = history[i+1];
		}
		history[MAX_HISTORY-1] = line;
	}

	public void kill() {
		try {
			out.println("EXT");
			cm.logOutUser(username, true);
			out.flush();
			out.close();
			in.close();
			this.socket.close();
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
	}

}